// Copyright (c) 2010 SuccessFactors, Inc.
// All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions
// are met:
//
//     * Redistributions of source code must retain the above
//       copyright notice, this list of conditions and the following
//       disclaimer.
//
//     * Redistributions in binary form must reproduce the above
//       copyright notice, this list of conditions and the following
//       disclaimer in the documentation and/or other materials
//       provided with the distribution.
//
//     * Neither the name of the SuccessFactors, Inc. nor the names of
//       its contributors may be used to endorse or promote products
//       derived from this software without specific prior written
//       permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
// FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
// COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
// INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
// (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
// HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
// OF THE POSSIBILITY OF SUCH DAMAGE.

package org.owasp.jxt;

import java.io.IOException;
import java.util.LinkedList;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;
import org.xml.sax.Attributes;
import org.xml.sax.EntityResolver;
import org.xml.sax.InputSource;
import org.xml.sax.Locator;
import org.xml.sax.SAXException;
import org.xml.sax.SAXParseException;
import org.xml.sax.XMLReader;
import org.xml.sax.helpers.XMLFilterImpl;

/**
 * XIncludeFilter
 *
 * @author Jeffrey Ichnowski
 * @version $Revision: 8 $
 */
class XIncludeFilter extends XMLFilterImpl {

    static final String XINCLUDE_NS = "http://www.w3.org/2001/XInclude";

    private SAXParserFactory _factory;
    private Locator _locator;
    private LinkedList<String> _includeStack = new LinkedList<String>();
    private int _xincludeDepth;

    public XIncludeFilter(SAXParserFactory factory) {
        _factory = factory;
    }

    public XIncludeFilter(SAXParserFactory factory, XMLReader reader) {
        super(reader);
        _factory = factory;
    }

    @Override
    public void parse(InputSource input) throws IOException, SAXException {
        _includeStack.addLast(input.getSystemId());
        try {
            super.parse(input);
        } finally {
            _includeStack.removeLast();
        }
    }

    @Override
    public void parse(String systemId) throws IOException, SAXException {
        _includeStack.addLast(systemId);
        try {
            super.parse(systemId);
        } finally {
            _includeStack.removeLast();
        }
    }

    @Override
    public void setDocumentLocator(Locator locator) {
        _locator = locator;
        super.setDocumentLocator(locator);
    }

    @Override
    public void startDocument() throws SAXException {
        super.startDocument();
    }

    @Override
    public void endDocument() throws SAXException {
        super.endDocument();
    }

    @Override
    public void characters(char[] ch, int start, int len)
        throws SAXException
    {
        if (_xincludeDepth == 0) {
            super.characters(ch, start, len);
        }
    }

    @Override
    public void ignorableWhitespace(char[] ch, int start, int len)
        throws SAXException
    {
        if (_xincludeDepth == 0) {
            super.ignorableWhitespace(ch, start, len);
        }
    }

    @Override
    public void skippedEntity(String name) throws SAXException {
        if (_xincludeDepth == 0) {
            super.skippedEntity(name);
        }
    }

    @Override
    public void startElement(String uri, String localName, String qName, Attributes atts)
        throws SAXException
    {
        if (XINCLUDE_NS.equals(uri) && "include".equals(localName)) {
            String href = atts.getValue("href");
            if (null == href) {
                throw new SAXParseException(
                    "include missing href attribute", _locator);
            }

            String parse = atts.getValue("parse");
            if (null == parse) {
                parse = "xml";
            }

            InputSource source = resolve(href);

            String encoding = atts.getValue("encoding");
            if (encoding != null) {
                source.setEncoding(encoding);
            }

            if ("xml".equals(parse)) {
                includeXML(source, encoding);
            } else if ("text".equals(parse)) {
                includeText(source, encoding);
            } else {
                throw new SAXParseException(
                    "invalid value for parse attribute, must be 'xml' or 'text'",
                    _locator);
            }

            _xincludeDepth++;
        } else if (_xincludeDepth > 0) {
            _xincludeDepth++;
        } else {
            super.startElement(uri, localName, qName, atts);
        }
    }

    @Override
    public void endElement(String uri, String localName, String qName)
        throws SAXException
    {
        if (_xincludeDepth > 0) {
            _xincludeDepth--;
        } else {
            super.endElement(uri, localName, qName);
        }
    }

    private InputSource resolve(String href) throws SAXException {
        String systemId = href;

        if (!href.startsWith("/")) {
            String baseUrl = _locator.getSystemId();
            if (baseUrl == null) {
                baseUrl = "/";
            }
            systemId = baseUrl.replaceAll("/[^/]*$", "/") + href;
        }

        if (_includeStack.contains(systemId)) {
            throw new SAXParseException(
                "Circular include", _locator);
        }

        EntityResolver resolver = getEntityResolver();

        // TODO: if (resolver == null) { ... }

        try {
            InputSource source = resolver.resolveEntity(null, systemId);

            if (source == null) {
                throw new SAXParseException(
                    "Could not resolve '"+href+"'", _locator);
            }

            return source;
        } catch (IOException e) {
            throw new SAXParseException(
                "Error resolving '"+href+"'", _locator, e);
        }
    }

    private void includeText(InputSource source, String encoding) throws SAXException {
        throw new SAXException("not implemented");
    }

    private void includeXML(InputSource source, String encoding) throws SAXException {
        try {
            SAXParser parser = _factory.newSAXParser();
            XMLReader reader = parser.getXMLReader();

            reader.setContentHandler(this);

            if (getEntityResolver() != null) {
                reader.setEntityResolver(getEntityResolver());
            }

            if (getErrorHandler() != null) {
                reader.setErrorHandler(getErrorHandler());
            }

            assert _xincludeDepth == 0 : "xinclude depth must be 0 here, but was "+_xincludeDepth;

            Locator oldLocator = _locator;
            _includeStack.addLast(source.getSystemId());
            try {
                reader.parse(source);
            } finally {
                _includeStack.removeLast();
                setDocumentLocator(oldLocator);
            }
        } catch (IOException e) {
            throw new SAXException(
                "IOException reading '"+source.getSystemId()+"'", e);
        } catch (ParserConfigurationException e) {
            throw new SAXException(
                "ParserConfigurationException reading '"+source.getSystemId()+"'", e);
        }
    }
} // XIncludeFilter
